package ru.spbsu.electromenu.storage.db;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import ru.spbsu.electromenu.model.Order;
import ru.spbsu.electromenu.storage.OrderStorage;
import ru.spbsu.electromenu.util.collections.Pair;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

/**
 * Date: 3/20/11
 * Time: 6:17 PM
 *
 * @author Artem Titov
 */
public class OrderDbStorage extends AbstractDbStorage implements OrderStorage {
    private static final Logger logger = LoggerFactory.getLogger(OrderDbStorage.class);

    private final RowMapper<Order> ORDER_ROW_MAPPER = new OrderRowMapper();

    private class OrderRowMapper implements RowMapper<Order> {
        @Override
        public Order mapRow(ResultSet rs, int i) throws SQLException {
            final Order order = new Order();
            order.setId(rs.getInt("Id"));
            order.setConfirmed(rs.getBoolean("Confirmed"));
            order.setSessionId(rs.getInt("SessionId"));
            order.setTime(rs.getDate("Time"));
            order.setOrderedDishesIds(loadDishes(order.getId()));
            return order;
        }

        private HashMap<Integer, Integer> loadDishes(final int orderId) {
            logger.debug("Getting ordered dishes for order with id {}", orderId);
            final List<Pair<Integer, Integer>> dishes = jdbcTemplate.query("select * from `OrderedDish` where `OrderId`=?",
                    new RowMapper<Pair<Integer, Integer>>() {
                        @Override
                        public Pair<Integer, Integer> mapRow(ResultSet rs, int i) throws SQLException {
                            return new Pair<Integer, Integer>(rs.getInt("DishId"), rs.getInt("Amount"));
                        }
                    },
                    orderId);
            if (dishes == null) {
                logger.warn("Failed to load ordered dishes");
                throw new IllegalStateException("Failed to load ordered dishes for order with id: " + orderId + ". JdbcTemplate return null");
            }
            logger.debug("{} ordered dishes are loaded", dishes.size());
            final HashMap<Integer, Integer> orderedDishes = new HashMap<Integer, Integer>();
            for (final Pair<Integer, Integer> orderedDish : dishes) {
                orderedDishes.put(orderedDish.getFirst(), orderedDish.getSecond());
            }
            return orderedDishes;
        }
    }

    @Override
    public Order get(int id) {
        return jdbcTemplate.queryForObject("select * from `Order` where `Id`=?", ORDER_ROW_MAPPER, id);
    }

    @Override
    public int put(final Order order) {
        return transactionTemplate.execute(new TransactionCallback<Integer>() {
            @Override
            public Integer doInTransaction(TransactionStatus transactionStatus) {
                logger.debug("Adding order to database");
                int affectedRows = jdbcTemplate.update("insert into `Order`(`Time`, `SessionId`, `Confirmed`) values (?, ?, ?)",
                        order.getTime(),
                        order.getSessionId(),
                        order.isConfirmed());
                if (affectedRows != 1) {
                    throw new IllegalStateException("No rows affected for the order");
                }
                logger.debug("Order added to database. Retrieving id");
                //todo check it
                order.setId(jdbcTemplate.queryForInt("select last_insert_id()"));
                logger.debug("Retrieved id: {}. Adding ordered dishes to database", order.getId());
                final Set<Integer> keySet = order.getOrderedDishesIds().keySet();
                for (final Integer dishId : keySet) {
                    logger.debug("Adding ordered dish. Id: {}", dishId);
                    affectedRows = jdbcTemplate.update("insert into `OrderedDish`(`OrderId`, `DishId`, `Amount`) values (?, ?, ?)",
                            order.getId(),
                            dishId,
                            order.getOrderedDishesIds().get(dishId));
                    if (affectedRows != 1) {
                        throw new IllegalStateException("Failed to add dish with id " + dishId +
                                " and amount " + order.getOrderedDishesIds().get(dishId) +
                                " for order with id " + order.getId());
                    }
                }
                return 1;
            }
        });
    }

    @Override
    public int update(final Order order) {
        return transactionTemplate.execute(new TransactionCallback<Integer>() {
            @Override
            public Integer doInTransaction(TransactionStatus transactionStatus) {
                logger.debug("Getting old order for id {} from database", order.getId());
                final Order oldOrder = get(order.getId());
                logger.debug("Updating order into database");
                int affectedRows = jdbcTemplate.update("update `Order` set `Time`=?, `SessionId`=?, `Confirmed`=? where `Id`=?",
                        order.getTime(),
                        order.getSessionId(),
                        order.isConfirmed(),
                        order.getId());
                if (affectedRows != 1) {
                    throw new IllegalStateException("No rows affected for the order");
                }
                logger.debug("Updating ordered dishes and adding new ordered dishes");
                final Set<Integer> keySet = order.getOrderedDishesIds().keySet();
                for (final Integer dishId : keySet) {
                    if (!oldOrder.getOrderedDishesIds().containsKey(dishId)) {
                        logger.debug("Adding new ordered dish. Id: {}", dishId);
                        affectedRows = jdbcTemplate.update("insert into `OrderedDish`(`OrderId`, `DishId`, `Amount`) values (?, ?, ?)",
                                order.getId(),
                                dishId,
                                order.getOrderedDishesIds().get(dishId));
                        if (affectedRows != 1) {
                            throw new IllegalStateException("Failed to add dish with id " + dishId +
                                    " and amount " + order.getOrderedDishesIds().get(dishId) +
                                    " for order with id " + order.getId());
                        }
                    } else if (order.getOrderedDishesIds().get(dishId).intValue() != oldOrder.getOrderedDishesIds().get(dishId)) {
                        logger.debug("Updating amount of already ordered dish. Id: {}", dishId);
                        affectedRows = jdbcTemplate.update("update `OrderedDish` set `Amount`=? where `OrderId`=? and `DishId`=?",
                                order.getOrderedDishesIds().get(dishId),
                                order.getId(),
                                dishId);
                        if (affectedRows != 1) {
                            throw new IllegalStateException("Failed to update dish with id " + dishId +
                                    " and amount " + order.getOrderedDishesIds().get(dishId) +
                                    " for order with id " + order.getId());
                        }
                    }
                }
                return 1;
            }
        });
    }

    @Override
    public int delete(int id) {
        return jdbcTemplate.update("delete from `Order` where `Id`=?", id);
    }

    @Override
    public List<Order> getALl() {
        return jdbcTemplate.query("select * from `Order`", ORDER_ROW_MAPPER);
    }

    @Override
    public List<Order> getConfirmed(final boolean confirmed) {
        return jdbcTemplate.query("select * from `Order` where Confirmed=?", ORDER_ROW_MAPPER, confirmed);
    }

    @Override
    public int deleteOrderedDish(final int orderId, final int dishId, final int amount) {
        final Order order = get(orderId);
        final int currentAmount = order.getOrderedDishesIds().get(dishId);
        if (currentAmount <= amount) {
            return jdbcTemplate.update("delete from `OrderedDish` where `OrderId`=? and `DishId`=? and `Amount`=?",
                    orderId, dishId, amount);
        } else {
            return jdbcTemplate.update("update `OrderedDish` set `Amount`=? where `OrderId`=? and `DishId`=?",
                    (currentAmount - amount), orderId, dishId);
        }
    }

    @Override
    public int confirm(final int orderId, final boolean confirm) {
        return jdbcTemplate.update("update `Order` set `Confirmed`=? where `Id`=?", confirm, orderId);
    }
}
